import threading, os, time, tempfile, cStringIO, hashlib
import errors
from fs.path import pathjoin, dirname, basename, pathsplit
from fs.osfs import OSFS

class NDFile:
    def __init__(self, fs, path, mode, pool, size=0, isnew=False, sha1=None):
        self.fs = fs
        self.path = path
        self.mode = mode
        self.pool = pool
        self.size = size
        self.cur = 0
        self.file = pool.inc(path, sha1=sha1)[0]
        self.closed = False
        self.touched = False
        self.isnew = isnew
        self.lock = threading.Lock()
        
        if isnew:
            self.file.upload()
            self.file.wait_for_upload()
            

    def __del__(self):
        self.close()
    
    def _close(self):
        pass
    
    def read(self, size=None):
        self.lock.acquire()
        if not self.closed:
            if 'a' in self.mode or 'r' in self.mode:
                self.file.download()
            data = self.file.read(self.cur, size)
            self.cur += len(data)
            if self.cur > self.size:
                self.size = self.cur
        self.lock.release()
        return data

    def write(self, data):
        self.lock.acquire()
        if not self.closed:
            if 'a' in self.mode or 'r' in self.mode:
                self.file.download()
            self.file.write(self.cur, data)
            self.cur = self.cur + len(data)
            if self.cur > self.size:
                self.size = self.cur
            self.touched = True
        self.lock.release()

    def seek(self, pos, where=os.SEEK_SET):
        if where == os.SEEK_SET:
            cur = pos
        elif where == os.SEEK_CUR:
            cur = self.cur + pos
        elif where == os.SEEK_END:
            cur = self.size + pos
        else:
            return
        if cur < 0:
            cur = 0
        if cur > self.size:
            cur = self.size
        self.lock.acquire()
        self.cur = cur
        self.lock.release()

    def tell(self):
        return self.cur

    def truncate(self, size=0):
        self.lock.acquire()
        self.file.truncate(size)
        self.size = size
        self.lock.release()
    
    def close(self):
        self.lock.acquire()
        if not self.closed:
            self.closed = True
            if self.touched:
                self.file.upload()
                #self.file.wait_for_upload()
                self.pool.dec(self.path)
        self._close()
        self.lock.release()


class NDFileDownloader(threading.Thread):
    def __init__(self, fs, path, f, lock=None, blocksize=4096):  
        super(NDFileDownloader, self).__init__()
        self.fs = fs
        self.path = path
        self.f = f
        self.lock = lock
        self.blocksize = blocksize
        self.len = 0
        self.rfile = None
        self.start()

    def _get_remote_file(self, path):
        raise errors.NDFSNeedToImplement
    
    def length(self):    
        return self.len

    def run(self):
        rfile = self._get_remote_file(self.path)
        if rfile is None:
            return
        while True:
            data = rfile.read(self.blocksize)
            size = len(data)
            if size > 0:
                if self.lock is not None:
                    self.lock.acquire()
                self.f.seek(self.len)
                self.f.write(data)
                self.f.flush()
                self.len += size
                if self.lock is not None:
                    self.lock.release()
            else:
                break

class NDFileUploader(threading.Thread):
    def __init__(self, fs, path, f, size):  
        super(NDFileUploader, self).__init__()
        self.fs = fs
        self.path = path
        self.f = f
        self.size = size
        self.start()

    def _write_remote_file(self, path, f, size):
        raise errors.NDFSNeedToImplement

    def run(self):
        self._write_remote_file(self.path, self.f, self.size)
        self.f.close()

class NDFilePooledFile(object):
    RETRY_NUM = 1000
    RETRY_INTEVAL = 1
    def __init__(self, pool, path, f, exist, downloader_class, uploader_class):
        self.pool = pool
        self.path = path
        self.file = f
        self.exist = exist
        self.downloader_class = downloader_class
        self.uploader_class = uploader_class
        self.downloader = None
        self.uploader = None
        self.lock = threading.Lock()
        
    def download(self):
        if (not self.exist) and (self.downloader is None):
            self.downloader = self.downloader_class(self.pool.fs, self.path, self.file, self.lock)
       
    def upload(self):
        self.lock.acquire()
        self.file.seek(0, os.SEEK_END)
        size = self.file.tell()
        f = NDFile(self.pool.fs, self.path, 'r', self.pool, size)
        self.uploader = self.uploader_class(self.pool.fs, self.path, f, size)
        self.lock.release()
        
    def truncate(self, size=0):
        self.file.truncate(size)
            
    def wait_for_upload(self):
        self.lock.acquire()
        # wait for uploading
        if self.uploader:
            while self.uploader.isAlive():
                self.lock.release()
                time.sleep(NDFilePooledFile.RETRY_INTEVAL)
                self.lock.acquire()
        self.lock.release()
        
    def wait_for_download(self, size):
        if self.exist:
            return
        self.lock.acquire()
        # wait for downloading
        for _ in range(NDFilePooledFile.RETRY_NUM):
            if self.downloader and self.downloader.isAlive():
                if size > self.downloader.length():# do wait
                    self.lock.release()
                    time.sleep(NDFilePooledFile.RETRY_INTEVAL)
                    self.lock.acquire()
                else:
                    break
            else:
                break
        self.lock.release()
        
    def read(self, index, size):
        self.wait_for_download(index + size)
        self.lock.acquire()
        self.file.seek(index)
        data = self.file.read(size)
        self.lock.release()
        return data

    def write(self, index, data):
        self.wait_for_download(index + len(data))
        self.lock.acquire()
        self.file.seek(index)
        self.file.write(data)
        self.file.flush()
        self.lock.release()


class NDFilePool(object):
    def __init__(self, fs, downloader_class, uploader_class):
        self.fs = fs
        self.m = {}
        self.downloader_class = downloader_class
        self.uploader_class = uploader_class
        self.lock = threading.Lock()

    def get(self, path):
        self.lock.acquire()
        v = self.m.get(path)
        self.lock.release()
        return v
   
    def _new_file(self, path, sha1=None):
        raise errors.NDFSNeedToImplement

    def new(self, path, sha1=None):
        self.lock.acquire()
        try:
            f, exist = self._new_file(path, sha1=sha1)
            f = NDFilePooledFile(self, path, f, exist, self.downloader_class, self.uploader_class)
            v = (f, 1)
            self.m[path] = v
        finally:
            self.lock.release()
        return v
        
    def inc(self, path, sha1=None):
        v = self.get(path)
        if v is None:
            return self.new(path, sha1=sha1)
        else:
            self.lock.acquire()
            self.m[path] = (v[0], v[1] + 1)
            self.lock.release()
        return v

    def dec(self, path):
        v = self.get(path)
        if v is not None:
            self.lock.acquire()
            self.m[path] = (v[0], v[1] - 1)
            self.lock.release()

    def clean(self):
        for k in self.m.keys():
            v = self.m[k]
            self.lock.acquire()
            if v[1] <= 0:
                try:
                    v[0].close()
                except:
                    pass
                try:
                    del self.m[k]
                except:
                    pass
            self.lock.release()


class NDFileTemporaryPool(NDFilePool):
    def __init__(self, fs, downloader_class, uploader_class, tmpdir=None):
        super(NDFileTemporaryPool, self).__init__(fs, downloader_class, uploader_class)
        self.tmpdir = tmpdir

    def _new_file(self, path, sha1=None):
        return tempfile.TemporaryFile(dir=self.tmpdir), False

class NDFileCachePool(NDFilePool):
    def __init__(self, fs, downloader_class, uploader_class, cachedir):
        super(NDFileCachePool, self).__init__(fs, downloader_class, uploader_class)
        self.cachedir = cachedir
        self.osfs = OSFS(cachedir)

    def _get_sha1(self, path):
        try:
            f = self.osfs.open(path, 'rb')
            s = hashlib.sha1()
            while True:
                data = f.read(4096)
                if len(data) > 0:
                    s.update(data)
                else:
                    break
            return s.hexdigest().upper()
        except:
            pass
        return None
         
   
    def _new_file(self, path, sha1=None):
        self.osfs.makedir(dirname(path), True, True)
        if self.osfs.exists(path) and sha1 is not None:
            if self._get_sha1(path) == sha1.upper():
                return self.osfs.open(path, "r+b"), True
        return self.osfs.open(path, "w+b"), False 
    

class NDFileMemoryPool(NDFilePool):
    def _new_file(self, path, sha1=None):
        return cStringIO.StringIO(), False


class NDFileMetaData(object):
    def __init__(self, fs, getter_class=None, case_insensitive=False, refresh_timeout=120):
        self.m = {}
        self.fs = fs
        self.lock = threading.Lock()
        if getter_class is not None:
            self.getter = getter_class(fs, self)
            self.getter.start()
        else:
            self.getter = None
        self.case_insensitive = case_insensitive
        self.refresh_timeout = refresh_timeout

    def __getitem__(self, key):
        return self.m.get(key)

    def __setitem__(self, key, value):
        self.m[key] = value

    def __delitem__(self, key):
        try:
            del self.m[key]
        except:
            pass
    
    def _add(self, path, v):
        raise errors.NDFSNeedToImplement
    
    def _update(self, p, v):
        if self.case_insensitive:
            p = p.upper()
        old = self.m.get(p)
        if old is None:
            self.m[p] = v
        else:
            for k in v.keys():
                old[k] = v.get(k)
    
    def update(self, p, v):
        self.lock.acquire()
        self._update(p, v)
        self.lock.release()
        
    def add(self, path, v):
        self.lock.acquire()
        v['_timestamp'] = time.time()
        v['_orignpath'] = path
        self._add(path, v)
        self.lock.release()
        
    def get_real_path(self, path):
        if self.case_insensitive:
            if '/' == path  or '' == path:
                return path
            self.lock.acquire()
            meta = self.m.get(path.upper())
            self.lock.release()
            if meta:
                return meta.get('_orignpath', path)
            else:
                return pathjoin(self.get_real_path(dirname(path)), basename(path))
        else:
            return path
            
    def refresh(self, path, v):
        now = time.time()
        if now - v.get('_timestamp', 0) > self.refresh_timeout:
            if self.getter is not None:
                self.getter.add(self.get_real_path(path))
            
    def get(self, path):
        if self.case_insensitive:
            p = path.upper()
        else:
            p = path
        self.lock.acquire()
        if p in self.m.keys():
            meta = self.m.get(p)
            self.lock.release()
            self.refresh(p, meta)
            return meta
        self.lock.release()
        if self.getter is not None:
            return self.getter.get(self.get_real_path(path))
        return None

    def force_get(self, path):
        if self.getter is not None:
            return self.getter.get(self.get_real_path(path))
        return self.get(path)
    
    def add_queue(self, path):
        if self.getter is not None:
            self.getter.add(self.get_real_path(path))

    def has(self, path):
        self.lock.acquire()
        r = path in self.m.keys()
        self.lock.release()
        return r
    
    def delitem(self, path):
        if self.case_insensitive:
            p = path.upper()
        else:
            p = path
        self.lock.acquire()
        del self.m[p]
        self.lock.release()
        
    def delitems(self, path):
        if self.case_insensitive:
            p = path.upper()
        else:
            p = path
        self.lock.acquire()
        for k in self.m.keys():
            if k.startswith(p):
                del self.m[k]
        self.lock.release()

class SmartQueue:
    
    def __init__(self, max_size=None):
        self._queue = []
        self.max_size = max_size
        
    def put(self, v):
        self._queue.append(v)
    
    def has(self, v):
        return v in self._queue
    
    def remove(self, v):
        try:
            self._queue.remove(v)
            return True
        except:
            pass
        return False
    
    def empty(self):
        return len(self._queue) <= 0
    
    def full(self):
        if self.max_size is None:
            return False
        else:
            return len(self._queue) >= self.max_size
    
    def size(self):
        return len(self._queue)
        
    def get(self):
        if not self.empty():
            r = self._queue[0]
            del self._queue[0]
        return r

class NDFileMetadataGetter(threading.Thread):
    def __init__(self, fs=None, metadata=None, queue_size=100):  
        super(NDFileMetadataGetter, self).__init__()
        self.fs = fs
        self.metadata = metadata
        self.queue = SmartQueue(queue_size)
        self.lock = threading.Lock()
        self.stopped = False

    def stop(self):
        self.stopped = True

    def _get_meta(self, path):
        raise errors.NDFSNeedToImplement
    
    def get(self, path):
        self.remove(path)
        v = self._get_meta(path)
        if self.metadata and v:
            self.metadata.add(path, v)
        return v
    
    def remove(self, path):
        self.lock.acquire()
        if not self.queue.empty():
            self.queue.remove(path)
        self.lock.release()
            
    def add(self, path):
        self.lock.acquire()
        if (not self.queue.full()) and (not self.queue.has(path)):
            self.queue.put(path)
        self.lock.release()
        
    def run(self):
        while self.stopped is False:
            self.lock.acquire() 
            if self.queue.empty():
                self.lock.release()
            else:
                path = self.queue.get()
                self.lock.release()
                self.get(path)
